Содержание

  • 1  Проект
    • 1.1  Описание проекта
    • 1.2  Загружаемые библиотеки
    • 1.3  Пользовательские функции
  • 2  Загрузка данных и общие сведения
    • 2.1  Выводы
  • 3  Предобработка данных и добавление расчетов
    • 3.1  Удаление полных дубликатов
    • 3.2  Замена типов данных
    • 3.3  Добавление расчетов
    • 3.4  Выводы
  • 4  Анализ данных
    • 4.1  Динамика событий по дням
    • 4.2  Уточнение анализируемого периода
    • 4.3  Изучение воронки событий
    • 4.4  Анализ результатов А/В-теста
  • 5  Общие выводы

Проект¶

Описание проекта¶

Рассматривается стартап - онлайн магазин по продаже продуктов питания. По имеющимуся логу событий мобильного приложения необходимо проанализировать действия пользователей при покупке товаров.

Основные задачи:

  • изучить воронку продаж,
  • узнать, как пользователи доходят до покупки,
  • сколько пользователей доходит до покупки, а сколько — «застревает» на предыдущих шагах (на каких именно),
  • исследовать результаты A/A/B-эксперимента (контрольные группы 246 и 247 - группы А/А-теста, 248 - тестовая группа В, в которой был изменен шрифт интерфейса).

Загружаемые библиотеки¶

In [1]:
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import scipy.stats as st
import numpy as np

Пользовательские функции¶

In [2]:
def mf_diff(ind):
    """ подсчет количества пользователей до и после отсечения данных в разрезе заданного атрибута """
    id_est = (
        df_data.pivot_table(index=ind, aggfunc={'id':'nunique'})
        .join(df_data2.pivot_table(index=ind, aggfunc={'id':'nunique'}), lsuffix='_before',rsuffix='_after')
    )
    id_est = id_est.append(id_est.apply('sum', axis=0).reset_index().set_index('index').rename({0:'Total'}, axis=1).T)
    id_est['diff']=id_est.id_before-id_est.id_after
    id_est['lost_share_proc'] = id_est.apply(lambda x: round(x['diff']*100/x['id_before'], 1) , axis=1)
    id_est.index.name=ind
    return id_est
In [3]:
def mf_bar(df, x_, y_, title, x_text, y_text):
    """ построение графика - гистограммы """
    fig=px.bar(df, x=x_, y=y_, text_auto=True)
    fig.update_layout(height=500, width=800, title_text=title)
    fig.update_xaxes(title_text=x_text)
    fig.update_yaxes(title_text=y_text)
    fig.show()
In [4]:
def mf_z_test(goal_list, all_list, alpha=0.05):
    """ Расчет p-value для биномиального распределения (z-тест)"""
    
    goal = np.array(goal_list) # числитель конверсии
    alll = np.array(all_list) # знаменатель конверсии

    p1 = goal[0]/alll[0] # пропорция успехов в 1 группе
    p2 = goal[1]/alll[1] # пропорция успехов во 2 группе
    p_combined = (goal[0]+ goal[1])/(alll[0] + alll[1]) # пропорция успехов в комбинированной группе:

    difference = p1 - p2  # разница пропорций в группах

    # считаем статистику в ст.отклонениях стандартного нормального распределения
    z_value = difference / ((p_combined * (1 - p_combined) * (1 / alll[0] + 1 / alll[1]))**0.5)

    distr = st.norm(0, 1) # задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)

    p_value = (1 - distr.cdf(abs(z_value))) * 2

    if p_value < alpha:
        result = 'H1'
    else:
        result = 'H0'

    return [p_value, alpha, result]

Загрузка данных и общие сведения¶

In [5]:
df_data = pd.read_csv('data.csv', sep="\t")

df_data.columns = [x.lower() for x in df_data.columns]
df_data.info()
print(f"\nОбъем занимаемой памяти датасета: {(df_data.memory_usage('deep')/(1024**2)).sum():.1f} Mb\n")
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 4 columns):
 #   Column          Non-Null Count   Dtype 
---  ------          --------------   ----- 
 0   eventname       244126 non-null  object
 1   deviceidhash    244126 non-null  int64 
 2   eventtimestamp  244126 non-null  int64 
 3   expid           244126 non-null  int64 
dtypes: int64(3), object(1)
memory usage: 7.5+ MB

Объем занимаемой памяти датасета: 7.5 Mb

In [6]:
# переименование столбцов к удобному виду
df_data.rename({'eventname':'event', 'deviceidhash':'id', 'eventtimestamp':'ts', 'expid':'gr'}, axis=1, inplace=True)
df_data.head()
Out[6]:
event id ts gr
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248
In [7]:
# анализ значений столбца групп
df_data.gr.value_counts()
Out[7]:
248    85747
246    80304
247    78075
Name: gr, dtype: int64
In [8]:
# проверка на полные дубликаты строк
print(f"Количество строк - полных дублей: {df_data.duplicated().sum()}")
Количество строк - полных дублей: 413

Выводы¶

  • названия столбцов заменены на более удобные в нижнем регистре,
  • пропусков в данных не найдено,
  • обнаружено 413 полных дублей строк, их необходимо удалить,
  • столбец с меткой даты "ts" будет преобразован к типу дата-время,
  • столбец с номером группы "gr" имеет всего три неотрицательных целочисленных значения, будет преобразован к типу unsigned.

Предобработка данных и добавление расчетов¶

Удаление полных дубликатов¶

In [9]:
df_data.drop_duplicates(inplace=True)
print(f"Количество строк - полных дублей: {df_data.duplicated().sum()}")
Количество строк - полных дублей: 0

Замена типов данных¶

In [10]:
# дата события преобразуется к типу дата-время
df_data.ts = pd.to_datetime(df_data.ts, unit='s')

# индекс группы преобазуется к целочисленному
df_data.gr = pd.to_numeric(df_data.gr, downcast='unsigned')
In [11]:
# итоговая структура датафрейма после преобразования
print(f"\nОбъем занимаемой памяти датасета после преобразования: {(df_data.memory_usage('deep')/(1024**2)).sum():.1f} Mb\n")
df_data.info()
Объем занимаемой памяти датасета после преобразования: 7.7 Mb

<class 'pandas.core.frame.DataFrame'>
Int64Index: 243713 entries, 0 to 244125
Data columns (total 4 columns):
 #   Column  Non-Null Count   Dtype         
---  ------  --------------   -----         
 0   event   243713 non-null  object        
 1   id      243713 non-null  int64         
 2   ts      243713 non-null  datetime64[ns]
 3   gr      243713 non-null  uint8         
dtypes: datetime64[ns](1), int64(1), object(1), uint8(1)
memory usage: 7.7+ MB

Добавление расчетов¶

In [12]:
# создается отдельный столбец дат
df_data['dt'] = df_data['ts'].dt.date

Выводы¶

  • полные дубли строк удалены из датасета,
  • столбец дат приведен к типу дата-время,
  • создан отдельный столбец дат "dt",
  • столбец групп преобразован к uint8.

Анализ данных¶

In [13]:
# всего событий в данных
df_data.event.value_counts().reset_index().rename({'index':'Событие','event':'Количество событий'}, axis=1)
Out[13]:
Событие Количество событий
0 MainScreenAppear 119101
1 OffersScreenAppear 46808
2 CartScreenAppear 42668
3 PaymentScreenSuccessful 34118
4 Tutorial 1018
In [14]:
# количество уникальных пользователей
print(f"Количество уникальных пользователей: {len(df_data.id.unique())}")
Количество уникальных пользователей: 7551
In [15]:
# среднее количество событий на пользователя
print("Медиана количества " +
      f"событий на пользователя: {df_data.pivot_table(index='id', aggfunc={'event':'count'}).median()[0]:.0f}")
Медиана количества событий на пользователя: 20

Промежуточные выводы

  • в данных представлено 5 событий, судя по названию и количеству зафиксированных событий MainScreenAppear является началом воронки, т.е. первое событие,
  • в среднем (медианная оценка) на пользователя приходится 20 событий, из которых 5 уникальных событий могут повторяться,
  • всего в логе представлено 7551 уникальный пользователь.

Динамика событий по дням¶

In [16]:
# диапазон дат в данных
print(f"Данные представлены с {df_data.dt.min()} по {df_data.dt.max()}")
Данные представлены с 2019-07-25 по 2019-08-07
In [17]:
# подготовка данных
temp = (
    df_data
    .pivot_table(index=df_data.ts.dt.round('1h'), aggfunc={'event':'count'})
    .reset_index()
)

# подготовка графика
fig = px.line(temp, x="ts", y="event", title='Динамика зафиксированных событий')
fig.update_xaxes(title_text="Дата")
fig.update_yaxes(title_text="Количество событий")
fig.show()

Промежуточные выводы

  • данные начали собираться с 25.07.2019 по 07.08.2019,
  • график динамики зафиксированных событий показал, что основной поток собираемой статистики стал поступать с 01.08.2019 по 07.08.2019, для дальнейшего анализа ограничимся данными этого периода.

Уточнение анализируемого периода¶

In [18]:
# отсекаются дни с неполными данными
df_data2 = df_data.query("ts >= '2019-08-01'")
print(f"Количество уникальных пользователей после отсечения данных: {df_data2.id.nunique()}")
Количество уникальных пользователей после отсечения данных: 7534
In [19]:
# оценка потери данных по пользователям в разрезе событий
mf_diff('event')
Out[19]:
id_before id_after diff lost_share_proc
event
CartScreenAppear 3749 3734 15 0.4
MainScreenAppear 7439 7419 20 0.3
OffersScreenAppear 4613 4593 20 0.4
PaymentScreenSuccessful 3547 3539 8 0.2
Tutorial 847 840 7 0.8
Total 20195 20125 70 0.3
In [20]:
# оценка потери данных по пользователям в разрезе групп
mf_diff('gr')
Out[20]:
id_before id_after diff lost_share_proc
gr
246 2489 2484 5 0.2
247 2520 2513 7 0.3
248 2542 2537 5 0.2
Total 7551 7534 17 0.2

Промежуточные выводы

  • изначально в датасете насчитывались логи по 7551 уникальному пользователю, после отсечения данных показатель снизился до 7534, т.е. на 17 пользователей стало меньше,
  • в целом снижение количества уникальных пользователе в разрезе событий лежит в диапазоне от 0.2% до 0.8% от их исходного количества, а по группам - от 0.2% до 0,3%, т.е. потери в данных ничтожны.

Изучение воронки событий¶

In [21]:
# подготовка данных
grd1 = (
    df_data2
    .pivot_table(index='event', aggfunc={'event':'count'})
    .rename({'event':'count'}, axis=1)
    .reset_index()
    .sort_values(by='count', ascending=False)
)
In [22]:
# подготовка графика
fig=px.pie(grd1, names='event', values='count')
fig.update_layout(height=500, width=500, title_text="Доли событий в общем объеме всех событий")
fig.show()
In [23]:
mf_bar(grd1, 'event', 'count', 'Гистограмма количества событий','Cобытия', 'Количество событий')

Промежуточные выводы

  • интерпретация событий:
    • 'MainScreenAppear' - показ главного экрана,
    • 'OffersScreenAppear' - показ экрана предложений (товаров),
    • 'CartScreenAppear' - показ экрана корзины,
    • 'PaymentScreenSuccessful - показ экрана успешной оплаты',
    • 'Tutorial' - показ экрана руководства,
  • событие 'Tutorial' исключим из анализа воронки, т.к. оно напрямую не влияет на ход продажи продуктов, так из 20125 событий только 1005 относится к 'Tutorial', что составляет примерно 0.5% от рассматриваемых событий.
In [24]:
# подготовка данных без этапа Tutorial
df_data3 = df_data.query("ts >= '2019-08-01' and event != 'Tutorial'")
In [25]:
# подсчет количества уникальных пользователей на каждом шаге воронки
grd2 = (
    df_data3.pivot_table(index='event', aggfunc={'id':'nunique'})
    .reset_index()
    .rename({'id':'count'}, axis=1)
    .sort_values(by='count', ascending=False)
    .reset_index(drop=True)
)

# подсчет конверсии к предыдущему событию
grd2['shift'] = (
    grd2['count']
    .shift(periods=1, axis=0)
)

grd2['conversion'] = grd2.apply(lambda x: round(x['count']*100/x['shift'],1), axis=1)
grd2['delta'] = grd2['shift'] - grd2['count']
grd2
Out[25]:
event count shift conversion delta
0 MainScreenAppear 7419 NaN NaN NaN
1 OffersScreenAppear 4593 7419.0 61.9 2826.0
2 CartScreenAppear 3734 4593.0 81.3 859.0
3 PaymentScreenSuccessful 3539 3734.0 94.8 195.0
In [26]:
# построение воронки продажи продуктов питания
fig = go.Figure(
    go.Funnel(y=list(grd2.reset_index()['event']), x=list(grd2.reset_index()['count'])
        ))
fig.update_layout(height=500, width=950, title = "Воронка продажи продуктов питания на сайте")    
fig.update_yaxes(title_text="Шаги воронки") 
    
fig.show()

Промежуточные выводы

  • самое большое количество уникальных пользователей теряется при переходе с события MainScreenAppear (главный экран) на OffersScreenAppear (экран предложения товаров), конверсия перехода от главного экрана к экрану предложения товара составила 62%,
  • на следующих переходах показатель конверсии к предыдущему шагу повышается (OffersScreenAppear -> CartScreenAppear: 81% и CartScreenAppear -> PaymentScreenSuccessful 95%), т.е. чем ближе покупатели к оплате сформированной корзины, тем больше доля успешных переходов на следующий шаг,
  • итоговая конверсия перехода от главного экрана до экрана оплаты (MainScreenAppear -> PaymentScreenSuccessful) составила 48%, т.е. половина пользователей попавших на главный экран совершат покупку с вероятностью почти 50%.

Анализ результатов А/В-теста¶

In [27]:
# проверка на переток пользователей из группы в группу
print("Количество пользователей, перешедших в ходе теста в другие группы: "+
      f"{df_data3.pivot_table(index='id', aggfunc={'gr':'nunique'}).query('gr>1').count()[0]}")

#df_data3.pivot_table(index='gr', aggfunc={'id':'nunique'})
Количество пользователей, перешедших в ходе теста в другие группы: 0
In [28]:
# количество пользователей, участвующих в А/В-тесте
grd=df_data3.pivot_table(index='gr', aggfunc={'id':'nunique'}).reset_index()
mf_bar(grd, 'gr', 'id', 'Количество уникальных групп по группам А/В-теста', 'Номер группы теста', 'Количество пользователей')
In [29]:
print("Проверка равенства числа пользователей по группам теста:")
print(f"246/247: {grd.set_index('gr').loc[246, 'id']/grd.set_index('gr').loc[247, 'id']:.3f}")
print(f"247/248: {grd.set_index('gr').loc[247, 'id']/grd.set_index('gr').loc[248, 'id']:.3f}")
print(f"246/248: {grd.set_index('gr').loc[246, 'id']/grd.set_index('gr').loc[248, 'id']:.3f}")
Проверка равенства числа пользователей по группам теста:
246/247: 0.988
247/248: 0.991
246/248: 0.979

Промежуточные выводы

  • пользователей, перешедших в ходе теста в другие группы, не найдено, т.е. разбиение пользователей на группы выполнено корректно,
  • количество уникальных пользователей по группе 246 (первая тестовая) - 2483 человека, 247 (вторая тестовая) - 2515, 248 (контрольная) - 2535,
  • значение показателя во всех группах должно быть одинаково, у нас есть небольшие отличия: 246/247 = 0.988, 247/248 = 0.991, 246/248 = 0.979,
  • различия по количеству пользователей в группах не должно превышать 1%, здесь оно чуть больше, что говорит о недостаточно качественном разбиении пользователей на группы, или эти пользователи были утеряны из-за отсечения данных по дате,
  • в целом, примем что количество пользователей в группах одинаково, что позволит принять результаты А/В теста.
In [30]:
# воронка продаж по трем группам
grd= (
    df_data3
    .pivot_table(index='event', columns='gr', aggfunc={'id':'nunique'})
    .droplevel(level=0, axis=1)
    .sort_values(by=246, ascending=False)
)
grd
Out[30]:
gr 246 247 248
event
MainScreenAppear 2450 2476 2493
OffersScreenAppear 1542 1520 1531
CartScreenAppear 1266 1238 1230
PaymentScreenSuccessful 1200 1158 1181
In [31]:
# построение сегментированной воронки
fig = go.Figure()
for i in grd.columns:  # по всем группам 
    fig.add_trace(go.Funnel(
        name = i,
        y = list(grd.index),
        x = list(grd.loc[:,i])
        )) 
fig.update_layout(title = "Воронка продажи продуктов питания на сайте по группам А/В теста")    
fig.update_yaxes(title_text="Шаги воронки") 
fig.show()

План анализа результатов А/В теста

  • выполним статистическую проверку гипотез по результатам А/А и А/В теста в комбинациях, групп:
    • группа 246 с группой 247 (А1/А2 тест)
    • 246 с 248 (А1/В тест),
    • 247 с 248 (А2/В тест),
    • 246 и 247 с 248 (А12/В тест),
  • для проверки статистических гипотез при сравнении долей применим z-test,
  • за нулевую гипотезу возмем утверждение: для выбранного шага воронки доли уникальных пользователей от всех уникальных пользоавтелей (конверсия) по группам совпадают,
  • альтернативная гипотеза: для выбранного шага воронки доли пользователей по группам различаются,
  • т.к. для проверки стат гипотез будут многократно использоваться одни и те же данные, т.е. выполняется множественное сравнение, то необходимо расчитать поправку уровня стат значимости.
In [32]:
# подготовка данных - количество уникальных пользователей по каждой группе по шагам воронки
df_t= df_data3.pivot_table(index='gr', columns='event', aggfunc={'id':'nunique'}).droplevel(level=0, axis=1)

# добавление совмещенных данных по 246 и 247 группам и сортировка по шагам воронки
df_t = (
    df_t.append(
        df_data3.query("gr in (246,247)")
        .pivot_table(index='event', aggfunc={'id':'nunique'})
        .rename({'id':'246_247'}, axis=1)
        .T
    )
    .T
    .sort_values(by=246, ascending=False)
    .T
)

    # добавление итоговых значений по группам
df_t['total']=(
    df_data3
    .pivot_table(index='gr', aggfunc={'id':'nunique'})
    .append(pd.DataFrame(df_data3.query("gr in (246,247)")['id'].nunique(), columns=['id'], index=['246_247']))
    .rename({'id':'total'}, axis=1)
)
    
print("Количество уникальных пользователей по группам и шагам воронки:")
df_t
Количество уникальных пользователей по группам и шагам воронки:
Out[32]:
event MainScreenAppear OffersScreenAppear CartScreenAppear PaymentScreenSuccessful total
246 2450 1542 1266 1200 2483
247 2476 1520 1238 1158 2512
248 2493 1531 1230 1181 2535
246_247 4926 3062 2504 2358 4995
In [33]:
# проверка долей пользователей по шагам воронки по группам
print("Проверка долей пользователей по шагам воронки по группам в процентах:")
df_t.apply(lambda x: x*100/x['total'], axis=1)
Проверка долей пользователей по шагам воронки по группам в процентах:
Out[33]:
event MainScreenAppear OffersScreenAppear CartScreenAppear PaymentScreenSuccessful total
246 98.670963 62.102296 50.986710 48.328635 100.0
247 98.566879 60.509554 49.283439 46.098726 100.0
248 98.343195 60.394477 48.520710 46.587771 100.0
246_247 98.618619 61.301301 50.130130 47.207207 100.0
In [34]:
# пороговый уровень стат значимости
alpha = 0.05

# поправка Шидака для 4 шагов воронки и 4 сравнений
alpha_sh = 1-(1-alpha)**(1/(4*4))
print(f"Пороговый уровень значимости после поправки Шидака: {alpha_sh:.2%}")
Пороговый уровень значимости после поправки Шидака: 0.32%
In [35]:
# расчет всех стат тестов
voc1={'246 с 247 (А1/А2)':[246, 247], '246 с 248 (А1/В)':[246, 248]
      , '247 c 248 (А2/В)':[247, 248], '246 и 247 с 248 (А12/В)':['246_247', 248]}
for j in voc1:
    groups = voc1[j]
    print(f"\n\n----  Сравнение групп {j} по шагам воронки:")
    for i in df_t.columns:
        if i !='total':
            res = mf_z_test([df_t.loc[groups[0], i], df_t.loc[groups[1], i]]
                      , [df_t.loc[groups[0], 'total'], df_t.loc[groups[1], 'total']], alpha_sh)
            if res[2] == 'H0':
                text = f"нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости {res[0]:.1%}"
            else:
                text = f"принята альтернативная гипотеза (доли разные) на уровне значимости {res[0]:.1%}"
            print(f"{i}:  \n    "+text)

----  Сравнение групп 246 с 247 (А1/А2) по шагам воронки:
MainScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 75.3%
OffersScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 24.8%
CartScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 22.9%
PaymentScreenSuccessful:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 11.4%


----  Сравнение групп 246 с 248 (А1/В) по шагам воронки:
MainScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 33.9%
OffersScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 21.4%
CartScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 8.1%
PaymentScreenSuccessful:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 21.7%


----  Сравнение групп 247 c 248 (А2/В) по шагам воронки:
MainScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 51.9%
OffersScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 93.3%
CartScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 58.8%
PaymentScreenSuccessful:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 72.8%


----  Сравнение групп 246 и 247 с 248 (А12/В) по шагам воронки:
MainScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 34.9%
OffersScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 44.6%
CartScreenAppear:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 18.7%
PaymentScreenSuccessful:  
    нулевая гипотеза не отвергнута (доли одинаковы) на уровне значимости 61.1%

Промежуточные выводы

  • на первом шаге воронки MainScreenAppear доля пользователей по всем группам составила чуть больше 98%, что говорит о том, что какие-то пользователи минуя его, попадают на следующие шаги (например, если человек сразу по поиску нашел нужный товар и перешел к корзине или оплате заказа),
  • значение критического уровня стат зачимости (альфа) выбрано 5%, с поправкой Шидака для 16 сравнений принят показатель, значение которого составило 0.32%,
  • разница в долях пользователей (конверсии) по шагам воронки для групп 246 и 247 (А/А тест) незначительна, что является необходимом условием корректности обработки данных для А/В теста:
    • для шага MainScreenAppear по 246 группы доля составила 98.7%, по 247 - 98.6%,
    • OffersScreenAppear - 62% и 60.5%,
    • CartScreenAppear - 51% и 49%,
    • PaymentScreenSuccessful - 48.3% и 46.1%,
  • в А/А тесте статистически значимой разницы по шагам воронки в группах 246 и 247 не найдено, что является обязательным условием для технической проверки сбора данных А/В теста, так:
    • уровень стат значимости шага MainScreenAppear составил 75.3%,
    • OffersScreenAppear - 24.8%,
    • CartScreenAppear - 22.9%,
    • PaymentScreenSuccessful - 11.4%,
  • при сравнении групп 246 и 248 по шагам воронки статистически значимой разницы не было найдено на соответствующих уровнях значимости (MainScreenAppear - 33.9%, OffersScreenAppear - 21.4%, CartScreenAppear - 8,1%, PaymentScreenSuccessful - 21.7%),
  • при сравнении групп 247 и 248 по шагам воронки статистически значимой разницы не было найдено на соответствующих уровнях значимости (MainScreenAppear - 51.9%, OffersScreenAppear - 93.3%, CartScreenAppear - 58.8%, PaymentScreenSuccessful - 72.8%),
  • при сравнении групп 246 + 247 и 248 по шагам воронки статистически значимой разницы не было найдено на соответствующих уровнях значимости (MainScreenAppear - 34.9%, OffersScreenAppear - 44.6%, CartScreenAppear - 18.7%, PaymentScreenSuccessful - 61.1%),
  • из результатов статистических тестов по всем шагам воронки ни одна нулевая гипотеза не была отвергнута, минимальное значение уровня значимости составило 8.1%, что существенно выше порогового значения в 0.32%,
  • в ходе А/В теста по каждому шагу продуктовой воронки ни по одной группе не было найдено статистически значимых различий, т.е. изменение шрифта в тестовой группе 248 никак не повлияло на конверсию по сравнению с контрольными группами.

Общие выводы¶

Предобработка и добавление расчетов

  • названия столбцов заменены на более удобные в нижнем регистре,
  • пропусков в данных не найдено,
  • обнаружено 413 полных дублей строк, были удалены,
  • столбец дат приведен к типу дата-время,
  • создан отдельный столбец дат "dt",
  • столбец групп преобразован к uint8.

Анализ данных

  • интерпретация событий:
    • 'MainScreenAppear' - показ главного экрана,
    • 'OffersScreenAppear' - показ экрана предложений (товаров),
    • 'CartScreenAppear' - показ экрана корзины,
    • 'PaymentScreenSuccessful - показ экрана успешной оплаты',
    • 'Tutorial' - показ экрана руководства,
  • в данных представлено 5 событий: исходя из названия события и анализа количества событий MainScreenAppear - первое событие,
  • количетсво событий Tutorial оказалось ничтожно мало и оно было исключено из анализа,
  • данные в логах собраны с с 25.07.2019 по 07.08.2019, однако основной поток собираемой статистики стал поступать с 01.08.2019 по 07.08.2019, который и был взят в качестве рабочего,
  • изначально в датасете насчитывались логи по 7551 уникальному пользователю, после отсечения данных показатель снизился до 7534, т.е. на 17 пользователей стало меньше,
  • в целом снижение количества уникальных пользователе в разрезе событий лежит в диапазоне от 0.2% до 0.8% от их исходного количества, а по группам - от 0.2% до 0,3%, т.е. потери в данных ничтожны,
  • самое большое количество уникальных пользователей теряется при переходе с события MainScreenAppear (главный экран) на OffersScreenAppear (экран предложения товаров), конверсия перехода от главного экрана к экрану предложения товара составила 62%,
  • на следующих переходах показатель конверсии к предыдущему шагу повышается (OffersScreenAppear -> CartScreenAppear: 81% и CartScreenAppear -> PaymentScreenSuccessful 95%), т.е. чем ближе покупатели к оплате сформированной корзины, тем больше доля успешных переходов на следующий шаг,
  • итоговая конверсия перехода от главного экрана до экрана оплаты (MainScreenAppear -> PaymentScreenSuccessful) составила 48%, т.е. половина пользователей попавших на главный экран совершат покупку с вероятностью почти 50%.

Анализ результатов А/В-теста

  • пользователей, перешедших в ходе теста в другие группы, не найдено, т.е. разбиение пользователей на группы выполнено корректно,
  • количество уникальных пользователей по группе 246 (первая тестовая) - 2483 человека, 247 (вторая тестовая) - 2515, 248 (контрольная) - 2535,
  • значение показателя во всех группах должно быть одинаково, у нас есть небольшие отличия: 246/247 = 0.988, 247/248 = 0.991, 246/248 = 0.979,
  • различия по количеству пользователей в группах не должно превышать 1%, здесь оно чуть больше, что говорит о недостаточно качественном разбиении пользователей на группы, или эти пользователи были утеряны из-за отсечения данных по дате,
  • в целом, примем что количество пользователей в группах одинаково, что позволит принять результаты А/В теста,
  • для проверки статистических гипотез при сравнении долей использовался z-test,
  • за нулевую гипотезу взято утверждение: для выбранного шага воронки доли уникальных пользователей от всех уникальных пользоавтелей (конверсия) по группам совпадают,
  • альтернативная гипотеза: для выбранного шага воронки доли пользователей по группам различаются,
  • на первом шаге воронки MainScreenAppear доля пользователей по всем группам составила чуть больше 98%, что говорит о том, что какие-то пользователи минуя его, попадают на следующие шаги (например, если человек сразу по поиску нашел нужный товар и перешел к корзине или оплате заказа),
  • значение критического уровня стат зачимости (альфа) выбрано 5%, с поправкой Шидака для 16 сравнений принят показатель, значение которого составило 0.32%,
  • разница в долях пользователей (конверсии) по шагам воронки для групп 246 и 247 (А/А тест) незначительна, что является необходимом условием корректности обработки данных для А/В теста:
    • для шага MainScreenAppear по 246 группы доля составила 98.7%, по 247 - 98.6%,
    • OffersScreenAppear - 62% и 60.5%,
    • CartScreenAppear - 51% и 49%,
    • PaymentScreenSuccessful - 48.3% и 46.1%,
  • в А/А тесте статистически значимой разницы по шагам воронки в группах 246 и 247 не найдено, что является обязательным условием для технической проверки сбора данных А/В теста, так:
    • уровень стат значимости шага MainScreenAppear составил 75.3%,
    • OffersScreenAppear - 24.8%,
    • CartScreenAppear - 22.9%,
    • PaymentScreenSuccessful - 11.4%,
  • при сравнении групп 246 и 248 по шагам воронки статистически значимой разницы не было найдено на соответствующих уровнях значимости (MainScreenAppear - 33.9%, OffersScreenAppear - 21.4%, CartScreenAppear - 8,1%, PaymentScreenSuccessful - 21.7%),
  • при сравнении групп 247 и 248 по шагам воронки статистически значимой разницы не было найдено на соответствующих уровнях значимости (MainScreenAppear - 51.9%, OffersScreenAppear - 93.3%, CartScreenAppear - 58.8%, PaymentScreenSuccessful - 72.8%),
  • при сравнении групп 246 + 247 и 248 по шагам воронки статистически значимой разницы не было найдено на соответствующих уровнях значимости (MainScreenAppear - 34.9%, OffersScreenAppear - 44.6%, CartScreenAppear - 18.7%, PaymentScreenSuccessful - 61.1%),
  • из результатов статистических тестов по всем шагам воронки ни одна нулевая гипотеза не была отвергнута, минимальное значение уровня значимости составило 8.1%, что существенно выше порогового значения в 0.32%,
  • в ходе А/В теста по каждому шагу продуктовой воронки ни по одной группе не было найдено статистически значимых различий, т.е. изменение шрифта в тестовой группе 248 никак не повлияло на конверсию по сравнению с контрольными группами.